package com.zws.cucumber.glue;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.jayway.jsonpath.*;
import com.zws.cucumber.expr.ZwsExpr;
import com.zws.cucumber.util.ZwsHttpUtil;
import com.zws.cucumber.util.colorfullog.ZwsColorfulLogger;
import com.zws.cucumber.util.jsonunit.ZwsVerifierMatcher;
import com.zws.cucumber.util.wiremock.datatable.ZwsMappingAndParameters;
import com.zws.cucumber.util.wiremock.datatable.ZwsMappingAndParametersTransformer;
import com.zws.cucumber.var.ZwsVar;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.DataTableType;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import net.javacrumbs.jsonunit.JsonAssert;
import net.javacrumbs.jsonunit.core.internal.JsonUtils;
import net.javacrumbs.jsonunit.core.internal.Node.JsonMap;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;

import static com.zws.cucumber.expander.ZwsStr.expand;
import static com.zws.cucumber.expr.ZwsExpr.evaluate;
import static java.util.stream.Collectors.*;
import static net.javacrumbs.jsonunit.JsonAssert.when;
import static net.javacrumbs.jsonunit.JsonAssert.withMatcher;
import static net.javacrumbs.jsonunit.core.Option.IGNORING_ARRAY_ORDER;
import static org.junit.jupiter.api.Assertions.*;

/**
 * author: zws
 */
public class ZwsHttpStepDef {

    private static final Logger logger = ZwsColorfulLogger.of(ZwsHttpStepDef.class);

    private static final ObjectMapper objectMapper = new ObjectMapper();

    private static final ZwsMappingAndParametersTransformer transformer = new ZwsMappingAndParametersTransformer();

    @Autowired
    protected RestTemplate restTemplate;

    @Value("http://localhost:${local.server.port}")
    protected String baseUrl;

    @DataTableType
    public List<ZwsMappingAndParameters> defineMappingAndParameters(DataTable dataTable) throws Throwable {
        return transformer.transform(dataTable);
    }

    @Given("prepare headers")
    public void prepareCustomizedHeaders(DataTable dataTable) {
        dataTable.cells().forEach(row -> {
            ZwsHttpUtil.prepareHeader(row.get(0), expand(row.get(1)));
        });
    }

    @Given("remove header {string}")
    public void removeHeader(String header) {
        ZwsHttpUtil.removeHeader(header);
    }

    @When("send {word} to url path {string}")
    public void exchange(String method, String urlPath) throws UnsupportedEncodingException {
        exchangeWithPlaintext(method, expand(urlPath), null);
    }

    @When("send {word} to url path {string} with body")
    public void exchangeWithBody(String method, String urlPath, String body)
        throws UnsupportedEncodingException {
        exchangeWithPlaintext(method, expand(urlPath), expand(body));
    }

    @When("send {word} to url path {string} with body in Content-Type {string}")
    public void exchangeWithBody(String method, String urlPath, String contentType, DataTable dataTable)
            throws UnsupportedEncodingException {
        switch (contentType) {
            case "multipart/form-data" -> exchangeWithMultiPartFormData(method, expand(urlPath), dataTable);
            default -> throw new IllegalArgumentException("Unsupported Content-Type: " + contentType);
        }

    }

    private void exchangeWithPlaintext(String method, String urlPath, String body)
        throws UnsupportedEncodingException {
        HttpEntity<String> requestEntity = new HttpEntity<>(body, ZwsHttpUtil.buildHttpHeaders());
        exchange0(method, urlPath, requestEntity);
    }

    private void exchangeWithMultiPartFormData(String method, String urlPath, DataTable dataTable) {

        final String HeaderContentDisposition = "Content-Disposition";
        final String HeaderContentType = "Content-Type";
        final String Data = "Data";

        ZwsHttpUtil.prepareHeaderIfAbsent(HttpHeaders.CONTENT_TYPE, MediaType.MULTIPART_FORM_DATA_VALUE);

        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();

        for (Map<String, String> row : dataTable.asMaps()) {
            Collection<String> unsupported = CollectionUtils.subtract(row.keySet(), Arrays.asList(HeaderContentDisposition, HeaderContentType, Data));
            if (!unsupported.isEmpty()) {
                if (unsupported.size() > 1) {
                    throw new IllegalArgumentException("Columns %s are not supported".formatted(unsupported));
                } else {
                    throw new IllegalArgumentException("Column %s is not supported".formatted(unsupported));
                }
            }
            Objects.requireNonNull(row.get(HeaderContentDisposition), "Column [Content-Disposition] must be present");
            Objects.requireNonNull(row.get(Data), "Column [Data] must be present");

            String valueOfHeaderContentDisposition = expand(row.get(HeaderContentDisposition));
            MultiValueMap<String, String> headersOfPart = new HttpHeaders();
            headersOfPart.add(HeaderContentDisposition, valueOfHeaderContentDisposition);
            if (row.containsKey(HeaderContentType) && StringUtils.isNotBlank(row.get(HeaderContentType))) {
                headersOfPart.add(HeaderContentType, expand(row.get(HeaderContentType)));
            }
            body.add(valueOfHeaderContentDisposition, new HttpEntity<>(evaluate("Data", expand(row.get(Data))), headersOfPart));
        }

        HttpEntity<MultiValueMap<String, Object>> requestEntity = new HttpEntity<>(body, ZwsHttpUtil.buildHttpHeaders());
        exchange0(method, urlPath, requestEntity);
    }

    private void exchange0(String method, String urlPath, HttpEntity<?> requestEntity) {
        String url = urlPath;
        if (!urlPath.startsWith("http://")) {
            url = baseUrl + urlPath;
        }
        logger.debug("exchange0()\n"
                        + "  method: {}\n"
                        + "  url: {}\n"
                        + "  headers: {}\n"
                        + "  body: {}",
                method, url, requestEntity.getHeaders(), requestEntity.getBody());

        ResponseEntity<byte[]> response = restTemplate.exchange(
                URI.create(url),
                HttpMethod.valueOf(method.toUpperCase()),
                requestEntity,
                byte[].class);
        ResponseEntity<?> newResponse = processResponse(response);

        ZwsVar.put("response", newResponse);
        ZwsVar.put(ZwsVar.VAR_RESP, newResponse);
    }


    private ResponseEntity<?> processResponse(ResponseEntity<byte[]> response) {
        String contentType = response.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE);
        byte[] raw = response.getBody();
        if (raw == null) {
            return response;
        }
        if (StringUtils.isNotBlank(contentType)) {
            switch (contentType) {
                case "application/zip":
                    try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(raw))) {
                        ZipEntry entry;
                        while ((entry = zis.getNextEntry()) != null) {
                            String name = entry.getName();
                            byte[] bytes = zis.readAllBytes();
                            ZwsVar.put(name, bytes);
                        }
                    } catch (IOException ex) {
                        throw new RuntimeException(ex);
                    }
                    return response;
                default:
                    // do nothing
                    break;
            }
        }
        return new ResponseEntity<>(new String(raw), response.getHeaders(), response.getStatusCode());
    }

    @Then("verify response's status {int}")
    public void verifyResponse(int expectedStatus) {
        ResponseEntity<String> respEntity = ZwsVar.get(ZwsVar.VAR_RESP);
        logger.debug("verifyResponse, actualStatus: {}, actualBody: {}",
            respEntity.getStatusCodeValue(), respEntity.getBody());
        assertEquals(expectedStatus, respEntity.getStatusCodeValue());
    }

    @Then("verify response's status {int} and no body")
    public void verifyResponseAndNoBody(int expectedStatus) {
        ResponseEntity<String> respEntity = ZwsVar.get(ZwsVar.VAR_RESP);
        logger.debug("verifyResponseAndNoBody, actualStatus: {}, actualBody: {}",
            respEntity.getStatusCodeValue(), respEntity.getBody());
        assertEquals(expectedStatus, respEntity.getStatusCodeValue());
        assertNull(respEntity.getBody());
    }

    @Then("verify response's error chain and status {int} and body")
    public void verifyErrorChainResponse(int expectedStatus, String expectedBodyPattern) throws  Exception {
        Map<String, Object> responseMap = objectMapper.readValue(expectedBodyPattern, Map.class);
        if ("[null]".equals(MapUtils.getString(responseMap, "error_chain"))) {
            logger.debug("begin to remove error_chain");
            responseMap.remove("error_chain");
            expectedBodyPattern = objectMapper.writeValueAsString(responseMap);
        }
        verifyResponse0(expectedStatus, new net.javacrumbs.jsonunit.core.Option[] { IGNORING_ARRAY_ORDER }, expectedBodyPattern);
    }
    @Then("verify response's status {int} and body")
    public void verifyResponse(int expectedStatus, String expectedBodyPattern) {
        verifyResponse0(expectedStatus, new net.javacrumbs.jsonunit.core.Option[] { IGNORING_ARRAY_ORDER }, expectedBodyPattern);
    }

    @Then("verify response's status {int} and body with json-unit's options {string}")
    public void verifyResponseWithOptions(int expectedStatus, String optionStr, String expectedBodyPattern) {
        net.javacrumbs.jsonunit.core.Option[] options = Stream.of(StringUtils.split(optionStr, ','))
                .map(String::trim)
                .map(String::toUpperCase)
                .map(net.javacrumbs.jsonunit.core.Option::valueOf)
                .toArray(net.javacrumbs.jsonunit.core.Option[]::new);
        verifyResponse0(expectedStatus, options, expectedBodyPattern);
    }

    @Then("verify response's status {int} and body without json-unit's options")
    public void verifyResponseWithoutOptions(int expectedStatus, String expectedBodyPattern) {
        verifyResponse0(expectedStatus, null, expectedBodyPattern);
    }

    public void verifyResponse0(int expectedStatus, net.javacrumbs.jsonunit.core.Option[] options, String expectedBodyPattern) {
        ResponseEntity<String> respEntity = ZwsVar.get(ZwsVar.VAR_RESP);
        if (StringUtils.isBlank(expectedBodyPattern)) {
            assertNull(respEntity.getBody());
        } else {
            String expectedBody = expand(expectedBodyPattern);
            logger.debug("verifyResponse, expectedBody: {}", expectedBody);
            logger.debug("verifyResponse, actualStatus: {}, actualBody: {}",
                    respEntity.getStatusCodeValue(), respEntity.getBody());
            assertEquals(expectedStatus, respEntity.getStatusCodeValue());
            if (options == null || options.length == 0) {
                JsonAssert.assertJsonEquals(
                        expectedBody,
                        respEntity.getBody(),
                        withMatcher("verifier", new ZwsVerifierMatcher()));
            } else if (options.length == 1) {
                JsonAssert.assertJsonEquals(
                        expectedBody,
                        respEntity.getBody(),
                        when(options[0]).withMatcher("verifier", new ZwsVerifierMatcher()));
            } else {
                JsonAssert.assertJsonEquals(
                        expectedBody,
                        respEntity.getBody(),
                        when(options[0], Arrays.copyOfRange(options, 1, options.length))
                                .withMatcher("verifier", new ZwsVerifierMatcher()));
            }
        }
    }

    @Then("verify response's header")
    public void verifyResponseHeader(DataTable table) {
        ResponseEntity<String> respEntity = ZwsVar.get(ZwsVar.VAR_RESP);
        List<List<String>> expectedHeaderList = table.asLists();
        Map<String, List<String>> expectedHeaderMap = expectedHeaderList
            .stream()
            .collect(groupingBy(l -> l.get(0), mapping(l -> l.get(1), toList())));
        Map<String, List<String>> actualHeaderMap = respEntity.getHeaders().entrySet()
            .stream()
            .collect(Collectors.toMap(Entry::getKey, Entry::getValue));

//        assertEquals(expectedHeaderMap.size(), actualHeaderMap.size());
        for (Entry<String, List<String>> expectedHeaderEntry : expectedHeaderMap.entrySet()) {
            String headerKey = expectedHeaderEntry.getKey();
            List<String> expectedValList = expectedHeaderEntry.getValue();
            if (expectedValList.size() == 1 && expectedValList.get(0).trim().equals("[null]")) {
                assertFalse(actualHeaderMap.containsKey(headerKey), "response contains header: " + headerKey);
            } else {
                assertTrue(actualHeaderMap.containsKey(headerKey),
                    "response doesn't contain header: " + headerKey);
                List<String> actualValList = new ArrayList<>(actualHeaderMap.get(headerKey));
                assertEquals(expectedValList.size(), actualValList.size(),
                    "response's header '" + headerKey + "' have different size");
                for (String expectedVal : expectedValList) {
                    String expectedVal0 = expand(expectedVal);
                    assertTrue(actualValList.removeIf(v -> ZwsExpr.verifySilently(headerKey, expectedVal0, v)),
                        "response's header '" + headerKey + "' doesn't have expected value, " + "expected: " + expectedVal0
                            + ", actual: " + actualHeaderMap.get(headerKey));
                }
            }
        }
    }

    @Deprecated
    @Given("extract to var {string} from response body using json path {string}")
    public void extractFromRespBodyUsingJsonPath(String var, String jsonPath) {
        ResponseEntity<String> resp = ZwsVar.get(ZwsVar.VAR_RESP);
        Object val = JsonPath.read(resp.getBody(), jsonPath);
        ZwsVar.put(var, val);
    }

    @Given("extract value from response body")
    public void extractFromResponseBody(DataTable table) {
        ResponseEntity<String> resp = ZwsVar.get(ZwsVar.VAR_RESP);
        for (List<String> pair : table.cells()) {
            String jsonPath = pair.get(0);
            String variableName = pair.get(1);

            Object val = JsonPath.read(resp.getBody(), jsonPath);
            ZwsVar.put(variableName, val);
        }
    }

    @Given("verify value in response body")
    public void verifyValueInResponseBody(DataTable table) {
        ResponseEntity<String> resp = ZwsVar.get(ZwsVar.VAR_RESP);
        DocumentContext context = JsonPath.parse(resp.getBody(), Configuration.defaultConfiguration().addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL));
        for (List<String> pair : table.cells()) {
            String jsonPath = pair.get(0);
            String expected = pair.get(1);
            if (!"[skip]".equals(jsonPath)) {
                Object actual = null;
                try {
                    actual = context.read(jsonPath);
                } catch (PathNotFoundException e) {
                    logger.warn(e.getMessage());
                }
                ZwsExpr.verify(jsonPath, expand(expected), actual);
            }
        }
    }

    @Given("verify extensions value in response body")
    public void verifyExtensionsValueInResponseBody(String jsonValues) {
        if (StringUtils.isBlank(jsonValues)) {
            return;
        }
        ResponseEntity<String> resp = ZwsVar.get(ZwsVar.VAR_RESP);
        DocumentContext context = JsonPath.parse(resp.getBody(), Configuration.defaultConfiguration().addOptions(Option.DEFAULT_PATH_LEAF_TO_NULL));
        Object value = JsonUtils.convertToJson(jsonValues, "expected").getValue();
        if (value instanceof JsonMap) {
            ((JsonMap)value).entrySet().forEach(entry -> {
                Object actual = context.read(entry.getKey());
                ZwsExpr.verify(entry.getKey(), expand((String)entry.getValue()), actual);
            });
        }
    }
}
